/** * Copyright (c) 2011 Christine Gerpheide <christine.ger@pheide.com> * * This code is distributed under the MIT License. Please see LICENSE.txt * for more details. */ package com.pheide.trainose; import java.net.URLEncoder; import java.sql.Timestamp; import java.util.HashMap; import java.util.List; import android.app.AlertDialog; import android.app.Dialog; import android.app.ExpandableListActivity; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.text.ClipboardManager; import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.view.ViewGroup; import android.widget.CursorTreeAdapter; import android.widget.ExpandableListAdapter; import android.widget.ExpandableListView; import android.widget.ExpandableListView.ExpandableListContextMenuInfo; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; /** * Class which displays timetables for a specific route. * * @author Christine Gerpheide */ public class Timetables extends ExpandableListActivity { private static final String TAG = "Timetables"; private static final int SYNC_ID = Menu.FIRST; private static final int SORT_ID = Menu.FIRST + 1; private static final int COPY_ID = Menu.FIRST + 2; private static final int DETAILS_ID = Menu.FIRST + 3; private static final int SEATS_ID = Menu.FIRST + 4; private static final int DELETE_ID = Menu.FIRST + 5; private static final int COPY_ALL_ID = Menu.FIRST + 6; static final int DIALOG_SORT_ID = 0; static final int DIALOG_DETAIL_ID = 1; static final int DIALOG_SEATS_ID = 2; static final int DIALOG_DELETE_ID = 3; TimetablesDbAdapter mTimetablesDbAdapter; long mRouteId; String mSourceTitle; String mDestinationTitle; long mTimetableId; ProgressDialog mDialog; public static List<HashMap<String,String>> timetablesList = null; private int mGroupIdColumnIndex; private ExpandableListAdapter mAdapter; private int mCurrentLegCount = 0; /** * Initialize this view. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.timetables_list); Bundle extras = getIntent().getExtras(); mRouteId = extras != null ? extras.getLong(RoutesDbAdapter.KEY_ROWID) : null; // Set title RoutesDbAdapter routesDbAdapter = new RoutesDbAdapter(this); routesDbAdapter.open(); Cursor routesCursor = routesDbAdapter.fetch(mRouteId); this.startManagingCursor(routesCursor); mSourceTitle = CursorHelper.getString(routesCursor, RoutesDbAdapter.KEY_SOURCE); mDestinationTitle = CursorHelper.getString(routesCursor, RoutesDbAdapter.KEY_DESTINATION); this.setTitle(mSourceTitle + " ⇨ " + mDestinationTitle); routesDbAdapter.close(); mTimetablesDbAdapter = new TimetablesDbAdapter(this); mTimetablesDbAdapter.open(); this.populateList(); this.registerForContextMenu(this.getExpandableListView()); } /* List methods */ /** * Populate the view with the timetables for this route with * a default sorting. */ private void populateList() { this.populateListSorted(null); this.setLastSynced(); } /** * Retrieve the last synced timestamp for this route and show it * in the view. */ private void setLastSynced() { RoutesDbAdapter routesDbAdapter = new RoutesDbAdapter(this); routesDbAdapter.open(); Cursor routesCursor = routesDbAdapter.fetch(mRouteId); long timestamp = CursorHelper.getLong(routesCursor, RoutesDbAdapter.KEY_TIMESTAMP); if (timestamp > 0) { Timestamp time = new Timestamp(timestamp); TextView lastSyncedTextView = (TextView) findViewById(R.id.last_synced); lastSyncedTextView.setText(time.toLocaleString()); } routesDbAdapter.close(); } /** * Populate the view with the timetables for this route with the * specified sorting. * * @param String the sorting to use for the timetables */ private void populateListSorted(String sorting) { Cursor timetablesCursor = mTimetablesDbAdapter.fetchByRouteSorted(mRouteId, sorting); this.startManagingCursor(timetablesCursor); mGroupIdColumnIndex = timetablesCursor.getColumnIndexOrThrow(TimetablesDbAdapter.KEY_ROWID); mAdapter = new MyExpandableListAdapter(timetablesCursor,this); this.setListAdapter(mAdapter); } /* Options menu */ /** * Create the options menu. */ @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, SYNC_ID, 0, R.string.optmenu_sync); menu.add(0, SORT_ID, 0, R.string.optmenu_sort); menu.add(0, DELETE_ID, 0, R.string.optmenu_delete); menu.add(0, COPY_ALL_ID, 0, R.string.optmenu_copy_all); return true; } /** * Handle when an option menu item is selected. */ @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch(item.getItemId()) { case SYNC_ID: this.syncRoute(); return true; case SORT_ID: this.showDialog(DIALOG_SORT_ID); return true; case DELETE_ID: this.showDialog(DIALOG_DELETE_ID); return true; case COPY_ALL_ID: this.copyAllTimetablesToClipboard(); return true; } return super.onMenuItemSelected(featureId, item); } /** * Copy all timetables for this route to the clipboard. */ protected void copyAllTimetablesToClipboard() { String text = this.getText(R.string.schedule) + " " + mSourceTitle + " " + this.getText(R.string.to) + " " + mDestinationTitle; Cursor timetablesCursor = mTimetablesDbAdapter.fetchByRoute(mRouteId); this.startManagingCursor(timetablesCursor); for (timetablesCursor.moveToFirst(); timetablesCursor.isAfterLast() == false; timetablesCursor.moveToNext()) { text += "\n" + this.getStringForTimetableFromCursor(timetablesCursor); } this.stopManagingCursor(timetablesCursor); this.copyTextToClipboard(text); } /* Context menu */ /** * Create the context menu for each timetable. */ @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.add(0, COPY_ID, 0, R.string.ctxmenu_copy); menu.add(0, SEATS_ID, 0, R.string.ctxmenu_seat_availability); } /** * Handler for pressing a context menu item for a timetable. */ @Override public boolean onContextItemSelected(MenuItem item) { ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) item.getMenuInfo(); mTimetableId = info.id; Log.w("TEST","" + mTimetableId); switch(item.getItemId()) { case COPY_ID: this.copyTimetableToClipboard(mTimetableId); return true; case DETAILS_ID: this.showDialog(DIALOG_DETAIL_ID); return true; case SEATS_ID: this.showDialog(DIALOG_SEATS_ID); return true; } return super.onContextItemSelected(item); } /** * Copy the selected timetable to the clipboard. * * @param long the ID of the timetable */ protected void copyTimetableToClipboard(long timetableId) { Cursor timetableCursor = mTimetablesDbAdapter.fetch(timetableId); this.startManagingCursor(timetableCursor); this.copyTextToClipboard(getStringForTimetableFromCursor(timetableCursor)); this.stopManagingCursor(timetableCursor); } /** * Open a web browser showing the seat availability for this route. */ protected void openSeatAvailability() { LegsDbAdapter legsDbAdapter = new LegsDbAdapter(this); legsDbAdapter.open(); Cursor legsCursor = legsDbAdapter.fetchByTimetable(mTimetableId); this.startManagingCursor(legsCursor); String url = "http://www.pheide.com/Services/TrainOse/seatAvailability.php?"; for (legsCursor.moveToFirst(); legsCursor.isAfterLast() == false; legsCursor.moveToNext()) { String source = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_SOURCE); String destination = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_DESTINATION); String depart = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_DEPART); String arrive = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_ARRIVE); String train = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_TRAIN) + " " + CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_TRAIN_NUM); try { url += "from[]=" + URLEncoder.encode(source,"UTF-8") + "&to[]=" + URLEncoder.encode(destination,"UTF-8") + "&depart[]=" + URLEncoder.encode(depart,"UTF-8") + "&arrive[]=" + URLEncoder.encode(arrive,"UTF-8") + "&trainNum[]=" + URLEncoder.encode(train,"UTF-8"); } catch (Exception e) { //TODO log encoding exception } } legsCursor.close(); Uri seatsAvailabilityUri = Uri.parse(url); Intent intent = new Intent(Intent.ACTION_VIEW, seatsAvailabilityUri); this.startActivity(intent); } /* Dialogs */ /** * Prepares any dynamic data needed within a dialog before it is displayed. * * @param int the Dialog ID * @param Dialog the dialog */ protected void onPrepareDialog(int id, Dialog dialog) { switch(id) { case DIALOG_DETAIL_ID: /* Cursor timetableCursor = mTimetablesDbAdapter.fetch(mTimetableId); this.startManagingCursor(timetableCursor); String delay = CursorHelper.getString(timetableCursor,TimetablesDbAdapter.KEY_DELAY); this.stopManagingCursor(timetableCursor); TextView tv = new TextView(this); tv.setText("Delay: " + delay); dialog.setContentView(tv); */ break; } } /** * Builder for all dialogs. * * @param int the ID of the dialog to build * @return the built dialog */ protected Dialog onCreateDialog(int id) { Dialog dialog; AlertDialog.Builder alertBuilder = new AlertDialog.Builder(this); switch(id) { case DIALOG_SORT_ID: final CharSequence[] items = {this.getString(R.string.depart), this.getString(R.string.arrive),this.getString(R.string.duration), this.getString(R.string.train)}; alertBuilder.setTitle(this.getString(R.string.sortBy)); alertBuilder.setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { String sorting; switch(item) { case 0: sorting = TimetablesDbAdapter.KEY_DEPART; break; case 1: sorting = TimetablesDbAdapter.KEY_ARRIVE; break; case 2: sorting = TimetablesDbAdapter.KEY_DURATION; break; case 3: sorting = TimetablesDbAdapter.KEY_TRAIN; break; default: sorting = null; } Timetables.this.populateListSorted(sorting); dialog.dismiss(); } }); dialog = alertBuilder.create(); break; case DIALOG_DETAIL_ID: dialog = new Dialog(this); dialog.setTitle("Details"); break; case DIALOG_SEATS_ID: alertBuilder.setMessage(R.string.seats_open_new_window) .setCancelable(false) .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { Timetables.this.openSeatAvailability(); } }) .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); dialog = alertBuilder.create(); break; case DIALOG_DELETE_ID: alertBuilder.setMessage(R.string.confirm_delete) .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { RoutesDbAdapter routesDbAdapter = new RoutesDbAdapter(Timetables.this); routesDbAdapter.open(); routesDbAdapter.delete(mRouteId); routesDbAdapter.close(); Timetables.this.finish(); } }) .setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); dialog = alertBuilder.create(); break; default: dialog = null; } return dialog; } /** * Called by synchronize button when there are no routes. */ public void syncRoute(View v) { this.syncRoute(); } /** * Initialize a background process to synchronize this route. */ protected void syncRoute() { new AsyncTask<Long, Void, Void>() { ProgressDialog mDialog; protected void onPreExecute() { mDialog = ProgressDialog.show(Timetables.this, "", Timetables.this.getString(R.string.sync_in_progress), true); } protected Void doInBackground(Long... routeIds){ TimetablesSynchronizer timetablesSynchronizer = new TimetablesSynchronizer(Timetables.this); timetablesSynchronizer.syncTimetablesForRoute(routeIds[0]); return null; } @Override protected void onPostExecute(Void result) { mDialog.dismiss(); Timetables.this.populateList(); } }.execute(mRouteId); } /* Helpers */ /** * Given a managed cursor, return a single timetable row as a readable string. * * @param Cursor A managed timetableCursor * @return the readable timetable string */ protected String getStringForTimetableFromCursor(Cursor timetableCursor) { // TODO implement as toString() functions // Copy information for each leg LegsDbAdapter legsDbAdapter = new LegsDbAdapter(this); legsDbAdapter.open(); Cursor legsCursor = legsDbAdapter.fetchByTimetable( CursorHelper.getLong(timetableCursor, TimetablesDbAdapter.KEY_ROWID)); this.startManagingCursor(legsCursor); String text = new String(); for (legsCursor.moveToFirst(); legsCursor.isAfterLast() == false; legsCursor.moveToNext()) { String source = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_SOURCE); String destination = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_DESTINATION); String depart = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_DEPART); String arrive = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_ARRIVE); String train = CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_TRAIN) + " " + CursorHelper.getString(legsCursor,LegsDbAdapter.KEY_TRAIN_NUM); text += source + " -> " + destination + "\n"; text += depart + " - " + arrive + " " + train + "\n"; } legsCursor.close(); return text; } /** * Copy the given text to the user's clipboard and show a toast. * * @param String the text to copy */ protected void copyTextToClipboard(String text) { // First add ad for this app text += "\n" + this.getText(R.string.copied_route_intro); ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); clipboard.setText(text); Toast.makeText(this, R.string.copy_succesful, Toast.LENGTH_SHORT).show(); } public class MyExpandableListAdapter extends CursorTreeAdapter { LayoutInflater mInflater; public MyExpandableListAdapter(Cursor cursor, Context context) { super(cursor, context); mInflater = LayoutInflater.from(context); } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { LegsDbAdapter legsDbAdapter = new LegsDbAdapter(Timetables.this); legsDbAdapter.open(); return legsDbAdapter.fetchByTimetable( CursorHelper.getLong(groupCursor, TimetablesDbAdapter.KEY_ROWID)); } @Override protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { TextView tv0 = (TextView) view.findViewById(R.id.count); tv0.setText(Integer.toString(mCurrentLegCount++) + '.'); TextView tv = (TextView) view.findViewById(R.id.depart); tv.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_DEPART)); TextView tv1 = (TextView) view.findViewById(R.id.arrive); tv1.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_ARRIVE)); TextView tv3 = (TextView) view.findViewById(R.id.train); tv3.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_TRAIN)); TextView tv7 = (TextView) view.findViewById(R.id.train_num); tv7.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_TRAIN_NUM)); TextView tv4 = (TextView) view.findViewById(R.id.source); tv4.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_SOURCE)); TextView tv5 = (TextView) view.findViewById(R.id.destination); tv5.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_DESTINATION)); TextView tv6 = (TextView) view.findViewById(R.id.delay); // For now, don't display the delay, since it would invalidate the day caching... //tv6.setText(CursorHelper.getString(cursor, LegsDbAdapter.KEY_DELAY)); tv6.setText(" "); } @Override protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { mCurrentLegCount = 1; // reset leg counter TextView tv = (TextView) view.findViewById(R.id.depart); tv.setText(CursorHelper.getString(cursor, TimetablesDbAdapter.KEY_DEPART)); TextView tv1 = (TextView) view.findViewById(R.id.arrive); tv1.setText(CursorHelper.getString(cursor, TimetablesDbAdapter.KEY_ARRIVE)); TextView tv2 = (TextView) view.findViewById(R.id.duration); tv2.setText(CursorHelper.getString(cursor, TimetablesDbAdapter.KEY_DURATION)); TextView tv3 = (TextView) view.findViewById(R.id.train); if (CursorHelper.getInt(cursor, TimetablesDbAdapter.KEY_NUM_LEGS) > 1) { tv3.setText("*"); } else { tv3.setText(CursorHelper.getString(cursor, TimetablesDbAdapter.KEY_TRAIN) + " " + CursorHelper.getString(cursor, TimetablesDbAdapter.KEY_TRAIN_NUM)); } } @Override protected View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { View view = mInflater.inflate(R.layout.legs_row, parent, false); return view; } @Override protected View newGroupView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { View view = mInflater.inflate(R.layout.timetables_row, parent, false); return view; } } }